package file

import (
	"context"
	"fmt"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/duke-git/lancet/v2/datetime"
	"github.com/duke-git/lancet/v2/fileutil"
	"github.com/suyuan32/simple-admin-common/enum/errorcode"
	"github.com/suyuan32/simple-admin-common/i18n"
	"github.com/suyuan32/simple-admin-common/utils/pointy"
	"github.com/suyuan32/simple-admin-common/utils/uuidx"
	"github.com/zeromicro/go-zero/core/errorx"
	"github.com/zeromicro/go-zero/core/logx"

	"github.com/suyuan32/simple-admin-file/pkg/svc"
	"github.com/suyuan32/simple-admin-file/pkg/upload"
	"github.com/suyuan32/simple-admin-file/pkg/utils/dberrorhandler"
	"github.com/suyuan32/simple-admin-file/pkg/utils/filex"
)

// FileUploader 负责文件上传
type FileUploader struct {
	logx.Logger
	svcCtx *svc.ServiceContext
}

// NewFileUploader 创建一个新的 FileUploader 实例
func NewFileUploader(svcCtx *svc.ServiceContext) *FileUploader {
	return &FileUploader{
		Logger: logx.WithContext(context.Background()),
		svcCtx: svcCtx,
	}
}

// UploadFile 实现文件上传功能
func (u *FileUploader) UploadFile(ctx context.Context, file multipart.File, handler *multipart.FileHeader) (*upload.UploadResponse, error) {
	// 从 upload.Config 获取配置信息
	config := u.svcCtx.Config.UploadConf

	// 校验后缀是否合法
	dotIndex := strings.LastIndex(handler.Filename, ".")
	if dotIndex == -1 {
		logx.Errorw("reject the file which does not have suffix")
		return nil, errorx.NewCodeInvalidArgumentError("file.wrongTypeError")
	}

	fileName, fileSuffix := handler.Filename[:dotIndex], handler.Filename[dotIndex+1:]
	fileUUID := uuidx.NewUUID()
	storeFileName := fileUUID.String() + "." + fileSuffix
	timeString := datetime.FormatTimeToStr(time.Now(), "yyyy-mm-dd")
	userId := ctx.Value("userId").(string)

	// 判断文件大小是否超过设定值
	fileType := strings.Split(handler.Header.Get("Content-Type"), "/")[0]
	if fileType != "image" && fileType != "video" && fileType != "audio" {
		fileType = "other"
	}

	err := filex.CheckOverSize(ctx, u.svcCtx, fileType, handler.Size)
	if err != nil {
		logx.Errorw("the file is over size", logx.Field("type", fileType),
			logx.Field("userId", userId), logx.Field("size", handler.Size),
			logx.Field("fileName", handler.Filename))
		return nil, err
	}

	// 生成路径
	publicStoreDir := filepath.Join(config.PublicStorePath,
		u.svcCtx.Config.Name, fileType, timeString)
	privateStoreDir := filepath.Join(config.PrivateStorePath,
		u.svcCtx.Config.Name, fileType, timeString)

	// 判断文件夹是否已创建, 同时创建好私人和公开文件夹防止文件状态改变时无法移动
	if !fileutil.IsExist(publicStoreDir) {
		err := fileutil.CreateDir(publicStoreDir + "/")
		if err != nil {
			logx.Errorw("failed to create directory for storing public files", logx.Field("path", publicStoreDir))
			return nil, errorx.NewCodeError(errorcode.Internal,
				u.svcCtx.Trans.Trans(ctx, i18n.Failed))
		}
	}

	if !fileutil.IsExist(privateStoreDir) {
		err := fileutil.CreateDir(privateStoreDir + "/")
		if err != nil {
			logx.Errorw("failed to create directory for storing private files", logx.Field("path", privateStoreDir))
			return nil, errorx.NewCodeError(errorcode.Internal,
				u.svcCtx.Trans.Trans(ctx, i18n.Failed))
		}
	}

	// 默认是公开的
	targetFile, err := os.Create(filepath.Join(publicStoreDir, storeFileName))
	if err != nil {
		logx.Errorw("fail to create file", logx.Field("path", filepath.Join(publicStoreDir, storeFileName)))
		return nil, errorx.NewCodeError(errorcode.Internal,
			u.svcCtx.Trans.Trans(ctx, i18n.Failed))
	}
	defer targetFile.Close()

	_, err = io.Copy(targetFile, file)
	if err != nil {
		logx.Errorw("fail to copy file", logx.Field("path", filepath.Join(publicStoreDir, storeFileName)))
		return nil, errorx.NewCodeError(errorcode.Internal,
			u.svcCtx.Trans.Trans(ctx, i18n.Failed))
	}

	// 存储在数据库
	relativePath := fmt.Sprintf("/%s/%s/%s/%s", u.svcCtx.Config.Name,
		fileType, timeString, storeFileName)

	var md5 string
	if handler.Header.Get("md5") != "" {
		md5 = handler.Header.Get("md5")
	} else {
		md5 = ""
	}

	var fileTagId uint64
	if handler.Header.Get("tagId") != "" {
		tagId, err := strconv.Atoi(handler.Header.Get("tagId"))
		if err != nil {
			return nil, errorx.NewCodeInvalidArgumentError("wrong tag ID")
		}
		fileTagId = uint64(tagId)
	}

	query := u.svcCtx.DB.File.Create().
		SetID(fileUUID).
		SetNotNilName(&fileName).
		SetNotNilFileType(pointy.GetPointer(filex.ConvertFileTypeToUint8(fileType))).
		SetNotNilPath(&relativePath).
		SetNotNilUserID(&userId).
		SetNotNilMd5(pointy.GetPointer(md5)).
		SetNotNilStatus(pointy.GetPointer(uint8(1))).
		SetNotNilSize(pointy.GetPointer(uint64(handler.Size)))

	if fileTagId != 0 {
		query = query.AddTagIDs(fileTagId)
	}

	err = query.Exec(ctx)

	if err != nil {
		return nil, dberrorhandler.DefaultEntError(u.Logger, err, "upload failed")
	}

	return &upload.UploadResponse{
		ID:           fileUUID.String(),
		Name:         handler.Filename,
		URL:          config.ServerURL + relativePath,
		RelativePath: relativePath,
		Size:         handler.Size,
		FileType:     fileType,
		UserId:       userId,
	}, nil
}
